/*
 * Copyright 2006-2018 The MZmine 2 Development Team
 * 
 * This file is part of MZmine 2.
 * 
 * MZmine 2 is free software; you can redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 * 
 * MZmine 2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with MZmine 2; if not,
 * write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
 * USA
 */

package net.sf.mzmine.modules.visualization.tic;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import javax.swing.JFrame;
import javax.swing.JMenuBar;
import javax.swing.filechooser.FileNameExtensionFilter;
import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartMouseListener;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTickUnit;
import org.jfree.chart.entity.ChartEntity;
import org.jfree.chart.entity.LegendItemEntity;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import com.google.common.base.Joiner;
import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
import net.sf.mzmine.datamodel.Feature;
import net.sf.mzmine.datamodel.RawDataFile;
import net.sf.mzmine.datamodel.Scan;
import net.sf.mzmine.desktop.Desktop;
import net.sf.mzmine.desktop.impl.WindowsMenu;
import net.sf.mzmine.main.MZmineCore;
import net.sf.mzmine.modules.visualization.spectra.simplespectra.SpectraVisualizerModule;
import net.sf.mzmine.parameters.ParameterSet;
import net.sf.mzmine.parameters.parametertypes.WindowSettingsParameter;
import net.sf.mzmine.parameters.parametertypes.selectors.ScanSelection;
import net.sf.mzmine.taskcontrol.Task;
import net.sf.mzmine.taskcontrol.TaskStatus;
import net.sf.mzmine.util.SimpleSorter;
import net.sf.mzmine.util.dialogs.LoadSaveFileChooser;

/**
 * Total ion chromatogram visualizer using JFreeChart library
 */
public class TICVisualizerWindow extends JFrame implements ActionListener {

  private static final long serialVersionUID = 1L;

  // CSV extension.
  private static final String CSV_EXTENSION = "csv";

  private TICToolBar toolBar;
  private TICPlot ticPlot;

  // Data sets
  private Hashtable<RawDataFile, TICDataSet> ticDataSets;

  private TICPlotType plotType;
  private ScanSelection scanSelection;
  private Range<Double> mzRange;

  private Desktop desktop;

  // Export file chooser.
  private static LoadSaveFileChooser exportChooser = null;

  /**
   * Constructor for total ion chromatogram visualizer
   */
  public TICVisualizerWindow(RawDataFile dataFiles[], TICPlotType plotType,
      ScanSelection scanSelection, Range<Double> mzRange, Feature[] peaks,
      Map<Feature, String> peakLabels) {

    super("Chromatogram loading...");

    assert mzRange != null;

    this.desktop = MZmineCore.getDesktop();
    this.plotType = plotType;
    this.ticDataSets = new Hashtable<RawDataFile, TICDataSet>();
    this.scanSelection = scanSelection;
    this.mzRange = mzRange;

    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    setBackground(Color.white);

    ticPlot = new TICPlot(this);
    add(ticPlot, BorderLayout.CENTER);

    toolBar = new TICToolBar(ticPlot);
    add(toolBar, BorderLayout.EAST);

    // add all peaks
    if (peaks != null) {

      for (Feature peak : peaks) {

        if (peakLabels != null && peakLabels.containsKey(peak)) {

          final String label = peakLabels.get(peak);
          ticPlot.addLabelledPeakDataset(new PeakDataSet(peak, label), label);

        } else {

          ticPlot.addPeakDataset(new PeakDataSet(peak));
        }
      }
    }

    // add all data files
    for (RawDataFile dataFile : dataFiles) {
      addRawDataFile(dataFile);
    }

    // Add the Windows menu
    JMenuBar menuBar = new JMenuBar();
    menuBar.add(new WindowsMenu());
    setJMenuBar(menuBar);

    pack();

    // get the window settings parameter
    ParameterSet paramSet =
        MZmineCore.getConfiguration().getModuleParameters(TICVisualizerModule.class);
    WindowSettingsParameter settings =
        paramSet.getParameter(TICVisualizerParameters.WINDOWSETTINGSPARAMETER);

    // update the window and listen for changes
    settings.applySettingsToWindow(this);
    this.addComponentListener(settings);

    // Listen for clicks on legend items
    ticPlot.addChartMouseListener(new ChartMouseListener() {
      @Override
      public void chartMouseClicked(ChartMouseEvent event) {
        ChartEntity entity = event.getEntity();
        XYPlot plot = (XYPlot) ticPlot.getChart().getPlot();

        if ((entity != null) && entity instanceof LegendItemEntity
            && plot.getRenderer().getClass().getName().indexOf(".TICPlotRenderer") > -1) {
          LegendItemEntity itemEntity = (LegendItemEntity) entity;
          XYLineAndShapeRenderer rendererAll = (XYLineAndShapeRenderer) plot.getRenderer();

          // Find index value
          int index = -1;
          for (int i = 0; i < plot.getDatasetCount(); i++) {
            if (rendererAll.getLegendItem(i, 1) != null && rendererAll.getLegendItem(i, 1)
                .getDescription().equals(itemEntity.getSeriesKey())) {
              index = i;
              break;
            }
          }
          XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) plot.getRenderer(index);

          // Select or deselect dataset
          Font font = new Font("Helvetica", Font.BOLD, 11);
          BasicStroke stroke = new BasicStroke(4);
          if (renderer.getDefaultLegendTextFont() != null
              && renderer.getDefaultLegendTextFont().isBold()) {
            font = new Font("Helvetica", Font.PLAIN, 11);
            stroke = new BasicStroke(1);
          }
          renderer.setDefaultLegendTextFont(font);
          renderer.setSeriesStroke(0, stroke);
        }
      }

      @Override
      public void chartMouseMoved(ChartMouseEvent event) {
        ChartEntity entity = event.getEntity();
        XYPlot plot = (XYPlot) ticPlot.getChart().getPlot();
        if ((entity != null) && entity instanceof LegendItemEntity
            && plot.getRenderer().getClass().getName().indexOf(".TICPlotRenderer") > -1) {
          ticPlot.setCursor(new Cursor(Cursor.HAND_CURSOR));
        } else {
          ticPlot.setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));
        }
      }
    });

  }

  void updateTitle() {

    NumberFormat rtFormat = MZmineCore.getConfiguration().getRTFormat();
    NumberFormat mzFormat = MZmineCore.getConfiguration().getMZFormat();
    NumberFormat intensityFormat = MZmineCore.getConfiguration().getIntensityFormat();

    StringBuffer mainTitle = new StringBuffer();
    StringBuffer subTitle = new StringBuffer();

    // If all data files have m/z range less than or equal to range of
    // the plot (mzMin, mzMax), then call this TIC, otherwise XIC
    Set<RawDataFile> fileSet = ticDataSets.keySet();
    String ticOrXIC = "TIC";

    // Enlarge range a bit to avoid rounding errors
    Range<Double> mzRange2 = Range.range(mzRange.lowerEndpoint() - 1, BoundType.CLOSED,
        mzRange.upperEndpoint() + 1, BoundType.CLOSED);
    for (RawDataFile df : fileSet) {
      if (!mzRange2.encloses(df.getDataMZRange())) {
        ticOrXIC = "XIC";
        break;
      }
    }

    if (plotType == TICPlotType.BASEPEAK) {
      if (ticOrXIC.equals("TIC")) {
        mainTitle.append("Base peak chromatogram");
      } else {
        mainTitle.append("XIC (base peak)");
      }
    } else {
      if (ticOrXIC.equals("TIC")) {
        mainTitle.append("TIC");
      } else {
        mainTitle.append("XIC");
      }
    }

    mainTitle.append(", m/z: " + mzFormat.format(mzRange.lowerEndpoint()) + " - "
        + mzFormat.format(mzRange.upperEndpoint()));

    CursorPosition pos = getCursorPosition();

    if (pos != null) {
      subTitle.append("Selected scan #");
      subTitle.append(pos.getScanNumber());
      if (ticDataSets.size() > 1) {
        subTitle.append(" (" + pos.getDataFile() + ")");
      }
      subTitle.append(", RT: " + rtFormat.format(pos.getRetentionTime()));
      if (plotType == TICPlotType.BASEPEAK) {
        subTitle.append(", base peak: " + mzFormat.format(pos.getMzValue()) + " m/z");
      }
      subTitle.append(", IC: " + intensityFormat.format(pos.getIntensityValue()));
    }

    // update window title
    RawDataFile files[] = ticDataSets.keySet().toArray(new RawDataFile[0]);
    Arrays.sort(files, new SimpleSorter());
    String dataFileNames = Joiner.on(",").join(files);
    setTitle("Chromatogram: [" + dataFileNames + "; " + mzFormat.format(mzRange.lowerEndpoint())
        + " - " + mzFormat.format(mzRange.upperEndpoint()) + " m/z" + "]");

    // update plot title
    ticPlot.setTitle(mainTitle.toString(), subTitle.toString());

  }

  /**
   * @return Returns the plotType.
   */
  TICPlotType getPlotType() {
    return plotType;
  }

  TICDataSet[] getAllDataSets() {
    return ticDataSets.values().toArray(new TICDataSet[0]);
  }

  /**
   */
  public void setRTRange(Range<Double> rtRange) {
    ticPlot.getXYPlot().getDomainAxis().setRange(rtRange.lowerEndpoint(), rtRange.upperEndpoint());
  }

  public void setAxesRange(double xMin, double xMax, double xTickSize, double yMin, double yMax,
      double yTickSize) {
    NumberAxis xAxis = (NumberAxis) ticPlot.getXYPlot().getDomainAxis();
    NumberAxis yAxis = (NumberAxis) ticPlot.getXYPlot().getRangeAxis();
    xAxis.setRange(xMin, xMax);
    xAxis.setTickUnit(new NumberTickUnit(xTickSize));
    yAxis.setRange(yMin, yMax);
    yAxis.setTickUnit(new NumberTickUnit(yTickSize));
  }

  /**
   * @see net.sf.mzmine.modules.RawDataVisualizer#setIntensityRange(double, double)
   */
  public void setIntensityRange(double intensityMin, double intensityMax) {
    ticPlot.getXYPlot().getRangeAxis().setRange(intensityMin, intensityMax);
  }

  /**
   * @see net.sf.mzmine.modules.RawDataVisualizer#getRawDataFiles()
   */
  public RawDataFile[] getRawDataFiles() {
    return ticDataSets.keySet().toArray(new RawDataFile[0]);
  }

  public void addRawDataFile(RawDataFile newFile) {

    final Scan scans[] = scanSelection.getMatchingScans(newFile);
    if (scans.length == 0) {
      desktop.displayErrorMessage(this, "No scans found.");
      return;
    }

    TICDataSet ticDataset = new TICDataSet(newFile, scans, mzRange, this);
    ticDataSets.put(newFile, ticDataset);
    ticPlot.addTICDataset(ticDataset);

  }

  public void removeRawDataFile(RawDataFile file) {
    TICDataSet dataset = ticDataSets.get(file);
    ticPlot.getXYPlot().setDataset(ticPlot.getXYPlot().indexOf(dataset), null);
    ticDataSets.remove(file);
  }

  /**
   * Export a file's chromatogram.
   *
   * @param file the file.
   */
  public void exportChromatogram(RawDataFile file) {

    // Get the data set.
    final TICDataSet dataSet = ticDataSets.get(file);
    if (dataSet != null) {

      // Create the chooser if necessary.
      if (exportChooser == null) {

        exportChooser = new LoadSaveFileChooser("Select Chromatogram File");
        exportChooser.addChoosableFileFilter(
            new FileNameExtensionFilter("Comma-separated values files", CSV_EXTENSION));
      }

      // Choose an export file.
      final File exportFile = exportChooser.getSaveFile(this, file.getName(), CSV_EXTENSION);
      if (exportFile != null) {

        MZmineCore.getTaskController().addTask(new ExportChromatogramTask(dataSet, exportFile));
      }
    }
  }

  /**
   * @return current cursor position
   */
  public CursorPosition getCursorPosition() {
    double selectedRT = ticPlot.getXYPlot().getDomainCrosshairValue();
    double selectedIT = ticPlot.getXYPlot().getRangeCrosshairValue();
    Enumeration<TICDataSet> e = ticDataSets.elements();
    while (e.hasMoreElements()) {
      TICDataSet dataSet = e.nextElement();
      int index = dataSet.getIndex(selectedRT, selectedIT);
      if (index >= 0) {
        double mz = 0;
        if (plotType == TICPlotType.BASEPEAK) {
          mz = dataSet.getZValue(0, index);
        }
        CursorPosition pos = new CursorPosition(selectedRT, mz, selectedIT, dataSet.getDataFile(),
            dataSet.getScanNumber(index));
        return pos;
      }
    }
    return null;
  }

  /**
   * @return current cursor position
   */
  public void setCursorPosition(CursorPosition newPosition) {
    ticPlot.getXYPlot().setDomainCrosshairValue(newPosition.getRetentionTime(), false);
    ticPlot.getXYPlot().setRangeCrosshairValue(newPosition.getIntensityValue());
  }

  /**
   * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
   */
  @Override
  public void actionPerformed(ActionEvent event) {

    String command = event.getActionCommand();

    if (command.equals("SHOW_SPECTRUM")) {
      CursorPosition pos = getCursorPosition();
      if (pos != null) {
        SpectraVisualizerModule.showNewSpectrumWindow(pos.getDataFile(), pos.getScanNumber());
      }
    }

    if (command.equals("MOVE_CURSOR_LEFT")) {
      CursorPosition pos = getCursorPosition();
      if (pos != null) {
        TICDataSet dataSet = ticDataSets.get(pos.getDataFile());
        int index = dataSet.getIndex(pos.getRetentionTime(), pos.getIntensityValue());
        if (index > 0) {
          index--;
          pos.setRetentionTime(dataSet.getXValue(0, index));
          pos.setIntensityValue(dataSet.getYValue(0, index));
          setCursorPosition(pos);

        }
      }
    }

    if (command.equals("MOVE_CURSOR_RIGHT")) {
      CursorPosition pos = getCursorPosition();
      if (pos != null) {
        TICDataSet dataSet = ticDataSets.get(pos.getDataFile());
        int index = dataSet.getIndex(pos.getRetentionTime(), pos.getIntensityValue());
        if (index >= 0) {
          index++;
          if (index < dataSet.getItemCount(0)) {
            pos.setRetentionTime(dataSet.getXValue(0, index));
            pos.setIntensityValue(dataSet.getYValue(0, index));
            setCursorPosition(pos);
          }
        }
      }
    }

  }

  @Override
  public void dispose() {

    // If the window is closed, we want to cancel all running tasks of the
    // data sets
    Task tasks[] = this.ticDataSets.values().toArray(new Task[0]);

    for (Task task : tasks) {
      TaskStatus status = task.getStatus();
      if ((status == TaskStatus.WAITING) || (status == TaskStatus.PROCESSING)) {
        task.cancel();
      }

    }
    super.dispose();

  }

  public TICToolBar getToolBar() {
    return toolBar;
  }

  public void setToolBar(TICToolBar toolBar) {
    this.toolBar = toolBar;
  }

  public TICPlot getTICPlot() {
    return ticPlot;
  }

  public void setTICPlot(TICPlot ticPlot) {
    this.ticPlot = ticPlot;
  }

}
